1 module targets.ios;
2 import common_macos;
3 import commons;
4 import global_opts;
5 
6 enum DefaultIphone = "iPhone 14";
7 enum DefaultIphoneOS = "16";
8 enum TARGET_TYPE = "simulator";
9 string getTargetArchitecture(bool isSimulator)
10 {
11 	import hconfigs;
12 	if(isSimulator)
13 	{
14 		static if(isARM)
15 			return "arm64";
16 		else
17 			return "x86_64";
18 	}
19 	return "arm64";
20 }
21 
22 string getExtraCommand(string type)
23 {
24 	if(type == "simulator") return " -sdk iphonesimulator ";
25 	return " -sdk iphoneos";
26 }
27 
28 
29 /**
30 *	Supports up to 99.9
31 */
32 int iosVersionToInt(string v)
33 {
34 	ulong power = 2;
35 	int ret = 0;
36 	foreach(ch; v)
37 	{
38         if(ch == '.')
39 			continue;
40 		ret+= (ch - '0') * 10^^power;
41 		power--;
42 	}
43 	return ret;
44 }
45 
46 string versionFromInt(long input)
47 {
48     ulong value = input;
49     char[] v;
50     
51     do
52     {
53         char rest = cast(char)(value % 10);
54         v~= '0' + rest;
55         value -= rest;
56         value/= 10;
57     }
58     while(value >= 1);
59     if(!v.length)
60         return null;
61     
62     
63     for(int i = 0; i < v.length / 2; i++)
64     {
65         char first = v[i];
66         int last = i + (cast(int)v.length - 1);
67         v[i] = v[last];
68         v[last] = first;
69     }
70 	if(v[$-1] != '0')
71 		return cast(string)v[0..$-1] ~ "." ~ v[$-1];
72 	else
73 		return cast(string)v[0..$-1];
74 }
75 
76 string trimCharacters(string input)
77 {
78 	import std.string;
79 	import std.ascii;
80 	input = input.chomp;
81 	int lower = 0;
82 	int upper = (cast(int)input.length - 1);
83 	if(!input.length)
84 		return input;
85 	while(lower < upper && !input[lower].isDigit)
86 		lower++;
87 	while(upper > lower && !input[upper].isDigit)
88 		upper--;
89 	if(upper - lower != 0)
90 		return input[lower..upper+1];
91 	return null;
92 }
93 
94 unittest
95 {
96 	assert(trimCharacters("16e") == "16");
97 	assert(iosVersionToInt("18.2") == 182);
98 	assert(versionFromInt(182) == "18.2");
99 }
100 
101 private string getDestination()
102 {
103 	// return "-destination 'generic/platform=iOS' ";
104 	return "-destination 'platform=iOS Simulator,name="~
105 	configs["iosDevice"].str ~
106 	",OS=" ~configs["iosVersions"].array[0].integer.versionFromInt ~ "' ";
107 }
108 private string getIosTriple(string arch, bool isSimulator)
109 {
110 	import hconfigs;
111 	static if(isARM)
112 	{
113 		if(isSimulator)
114 			return "arm64-apple-ios14.0-simulator";
115 	}
116 	return arch~"-apple-ios14.0";
117 }
118 
119 string getIosLibFolder(bool isSimulator)
120 {
121 	version(X86_64)
122 	{
123 		if(isSimulator)
124 			return "ios-x86_64";
125 	}
126 	else
127 	{
128 		if(isSimulator)
129 			return "ios-arm64-simulator";
130 	}
131 	return "ios-arm64";
132 }
133 ChoiceResult putInstalledDeviceInformation(ref Terminal t)
134 {
135 	import std.process;
136 	import std.json;
137 	import commons;
138 	///A way to get which devices are registered in the user PC
139 	auto res = executeShell("xcrun simctl list runtimes --json");
140 	if(res.status)
141 	{
142 		t.writelnError("No iOS runtime found? Please try opening XCode and installing the iOS runtime.");
143 		return ChoiceResult.Error;
144 	}
145 	JSONValue json = parseJSON(res.output);
146 	int[] iosVersions;
147 	int bestVersion = 0;
148 
149 	foreach(JSONValue v; json["runtimes"].array)
150 	{
151 		import std.string;
152 		import std.conv;
153 		if(!v["isAvailable"].boolean || v["platform"].str != "iOS")
154 			continue;
155 		iosVersions~= v["version"].str.iosVersionToInt;
156 		
157 		foreach(JSONValue device; v["supportedDeviceTypes"].array)
158 		{
159 			if(device["productFamily"].str != "iPhone" || !device["identifier"].str.startsWith("com.apple.CoreSimulator.SimDeviceType"))
160 				continue;
161 			string[] nameParts = device["name"].str.split(" ");
162 			if(nameParts.length < 2)
163 				continue;
164 			nameParts[1] = nameParts[1].trimCharacters;
165 			if(nameParts[1].length)
166 			{
167 				int currVersion = nameParts[1].to!int;
168 				bestVersion = currVersion > bestVersion ? currVersion : bestVersion;
169 			}
170 		}
171 	}
172 	import std.algorithm.sorting;
173 	import std.array;
174 	import std.conv;
175 
176 	configs["iosVersions"] = JSONValue(iosVersions.sort.array);
177 	configs["iosDevice"] = JSONValue("iPhone " ~ bestVersion.to!string);
178 	updateConfigFile();
179 
180 	return ChoiceResult.Continue;
181 }
182 
183 ChoiceResult prepareiOS(Choice* c, ref Terminal t, ref RealTimeConsoleInput input, in CompilationOptions cOpts)
184 {
185 	bool isSimulator = TARGET_TYPE == "simulator";
186 	string arch = getTargetArchitecture(isSimulator);
187 	prepareAppleOSBase(c,t,input);
188 
189 	string out_extraLinkerFlags;
190 	setupPerCompiler(t, "ldc2", getIosLibFolder(isSimulator), out_extraLinkerFlags);
191 	injectLinkerFlagsOnXcode(t, input, out_extraLinkerFlags);
192 	if(!("lastUser" in configs))
193 	{
194 		configs["lastUser"] = environment["USER"];
195 		configs["firstiOSRun"] = true;
196 	}
197 	if(environment["USER"] != configs["lastUser"].str)
198 	{
199 		configs["firstiOSRun"] = true;
200 	}
201 	if("iosDevice" !in configs || "iosVersions" !in configs || !configs["iosVersions"].array.length)
202 		putInstalledDeviceInformation(t);
203 	
204 	appleClean = configs["firstiOSRun"].boolean;
205 
206 	string codeSignCommand = getCodeSignCommand(t);
207 	string extraCommands = getExtraCommand(TARGET_TYPE);
208 
209 	with(WorkingDir(configs["gamePath"].str))
210 	{
211 		cleanAppleOSLibFolder();
212 		ProjectDetails d;
213 		if(waitRedub(t, DubArguments().
214 			command("build").configuration("ios").arch(getIosTriple(arch, isSimulator)).compiler("ldc2").opts(cOpts),
215 			d,
216 			getHipPath("build", "appleos", XCodeDFolder, "libs")) != 0)
217 		{
218 			t.writelnError("Could not build for AppleOS.");
219 			return ChoiceResult.Error;
220 		}
221 		string clean = appleClean ? "clean " : "";
222 
223 		with(WorkingDir(getHipPath("build", "appleos")))
224 		{
225 			t.wait(spawnShell(
226 				"xcodebuild -jobs 8 -configuration Debug -scheme 'HipremeEngine iOS' " ~
227 				clean ~
228 				"build CONFIGURATION_BUILD_DIR=\"bin\" "~ 
229 				codeSignCommand ~ extraCommands ~
230 				getDestination()
231 			));
232 
233 			string device = `"`~configs["iosDevice"].str~`"`;
234 			t.wait(spawnShell(
235 				"xcrun simctl bootstatus " ~ device~ " -b || xcrun simctl boot " ~ device ~
236 				"&& open -a Simulator && "~
237 				"xcrun simctl install booted " ~ getHipPath("build", "appleos", "bin", "HipremeEngine.app") ~ " && " ~
238 				"xcrun simctl launch --console booted hipreme.HipremeEngine"
239 			));
240 		}
241 	}
242 	if(configs["firstiOSRun"].boolean)
243 	{
244 		configs["firstiOSRun"] = false;
245 		updateConfigFile();
246 	}
247 	return ChoiceResult.None;
248 }